Skip to main content

Blotter

Learn how the Blotter system works and how to use slippage models, commission models, and custom order routing.

Overview

The Blotter sits between your strategy and the order execution layer. Every order you create — whether via create_limit_order(), create_market_order(), or batch_order() — flows through the blotter before reaching the OrderService.

Strategy (Context)


┌────────┐
│ Blotter │ ← slippage, commission, routing
└────┬───┘


OrderService → OrderExecutor → Exchange / Simulation

The framework automatically selects a blotter if you don't set one:

ModeDefault BlotterBehavior
Live tradingDefaultBlotterPass-through — no slippage or commission
BacktestingSimulationBlotterConfigurable slippage and commission models

You can override the default by calling app.set_blotter(...) with any Blotter subclass.

Slippage Models

Slippage models determine how the execution price deviates from the intended order price. They are used by the SimulationBlotter during backtesting.

NoSlippage (default)

Orders fill at the exact intended price.

from investing_algorithm_framework import SimulationBlotter, NoSlippage

app.set_blotter(SimulationBlotter(
slippage_model=NoSlippage()
))

PercentageSlippage

Buy orders fill at a slightly higher price, sell orders at a slightly lower price.

from investing_algorithm_framework import SimulationBlotter, PercentageSlippage

# 0.1% slippage
app.set_blotter(SimulationBlotter(
slippage_model=PercentageSlippage(percentage=0.001)
))

For a buy order at price 100.0 with 0.1% slippage, the fill price becomes 100.10. For a sell order, it becomes 99.90.

FixedSlippage

Adds or subtracts a fixed amount from the order price.

from investing_algorithm_framework import SimulationBlotter, FixedSlippage

# $0.05 slippage per order
app.set_blotter(SimulationBlotter(
slippage_model=FixedSlippage(amount=0.05)
))

Custom Slippage Model

Create your own by extending SlippageModel:

from investing_algorithm_framework import SlippageModel

class VolumeWeightedSlippage(SlippageModel):
def __init__(self, base_pct=0.001, volume_factor=0.0001):
self.base_pct = base_pct
self.volume_factor = volume_factor

def calculate_slippage(self, price, order_side, amount=None):
pct = self.base_pct
if amount is not None:
pct += self.volume_factor * amount

if order_side == "BUY":
return price * (1 + pct)
return price * (1 - pct)

Commission Models

Commission models determine the fee charged for each trade. They are used by the SimulationBlotter during backtesting.

NoCommission (default)

Zero fees on all trades.

from investing_algorithm_framework import SimulationBlotter, NoCommission

app.set_blotter(SimulationBlotter(
commission_model=NoCommission()
))

PercentageCommission

Fee is a percentage of the total trade value (price × amount).

from investing_algorithm_framework import SimulationBlotter, PercentageCommission

# 0.1% commission
app.set_blotter(SimulationBlotter(
commission_model=PercentageCommission(percentage=0.001)
))

FixedCommission

A fixed fee per trade, regardless of trade size.

from investing_algorithm_framework import SimulationBlotter, FixedCommission

# $1.00 per trade
app.set_blotter(SimulationBlotter(
commission_model=FixedCommission(amount=1.0)
))

Custom Commission Model

Create your own by extending CommissionModel:

from investing_algorithm_framework import CommissionModel

class TieredCommission(CommissionModel):
def calculate_commission(self, price, amount, order_side):
trade_value = price * amount
if trade_value > 10000:
return trade_value * 0.0005 # 0.05% for large trades
return trade_value * 0.001 # 0.1% for small trades

SimulationBlotter

The SimulationBlotter applies slippage and commission models to every order and records each fill as a Transaction.

from investing_algorithm_framework import (
SimulationBlotter,
PercentageSlippage,
PercentageCommission,
)

app.set_blotter(SimulationBlotter(
slippage_model=PercentageSlippage(0.001), # 0.1% slippage
commission_model=PercentageCommission(0.001), # 0.1% commission
))
Automatic Setup

If you don't set a blotter and run a backtest, the framework automatically uses a SimulationBlotter with NoSlippage and NoCommission.

Transactions

Every order placed through the blotter is recorded as a Transaction. Transactions provide an audit trail of all fills, including the actual execution price, slippage, and commission.

class MyStrategy(TradingStrategy):
def run_strategy(self, algorithm, market_data):
# Place some orders...
self.create_limit_order(
target_symbol="BTC", price=50000, amount=0.1
)

# Get all transactions recorded by the blotter
transactions = self.get_transactions()

for tx in transactions:
print(f"{tx.symbol} {tx.order_side}: "
f"price={tx.price}, amount={tx.amount}, "
f"commission={tx.commission}, slippage={tx.slippage}")

Each Transaction contains:

FieldDescription
order_idID of the order
symbolTarget symbol (e.g. "BTC")
order_side"BUY" or "SELL"
priceActual fill price (after slippage)
amountFill amount
costTotal cost (price × amount)
commissionCommission charged
slippageSlippage amount (abs(fill_price - intended_price))
timestampUTC timestamp of the fill

You can serialize a transaction with tx.to_dict().

Batch Orders

The batch_order() method lets you place multiple orders at once through the blotter:

class MyStrategy(TradingStrategy):
def run_strategy(self, algorithm, market_data):
orders = [
{
"target_symbol": "BTC",
"order_side": "BUY",
"price": 50000,
"amount": 0.1,
},
{
"target_symbol": "ETH",
"order_side": "BUY",
"price": 3000,
"amount": 1.0,
},
]
created_orders = self.batch_order(orders)

The default implementation places orders sequentially. Override batch_order() in a custom blotter for atomic batch behavior or smart order routing.

Custom Blotter

Create your own blotter by extending the Blotter class and implementing place_order() and cancel_order():

from investing_algorithm_framework import Blotter

class SmartOrderRouter(Blotter):
def place_order(self, order_data, context):
"""
Custom order routing logic.
"""
symbol = order_data.get("target_symbol")

# Example: route large orders differently
amount = order_data.get("amount", 0)
if amount > 100:
# Split into smaller orders
half = amount / 2
order_data["amount"] = half
order1 = context.order_service.create(order_data)
order2 = context.order_service.create(order_data)
return order1 # Return the first order

return context.order_service.create(order_data)

def cancel_order(self, order_id, context):
"""
Cancel a specific order.
"""
order = context.order_service.get(order_id)
if order is None:
raise Exception(f"Order {order_id} not found")

context.order_service.update(
order_id, {"status": "CANCELED"}
)
return context.order_service.get(order_id)

# Register the custom blotter
app.set_blotter(SmartOrderRouter())

Blotter API

MethodRequiredDescription
place_order(order_data, context)YesPlace a single order. Must be implemented.
cancel_order(order_id, context)YesCancel an order. Must be implemented.
batch_order(orders_data, context)NoPlace multiple orders. Default calls place_order() sequentially.
get_open_orders(context, target_symbol)NoGet open orders. Default delegates to context.
get_transactions()NoGet recorded transactions.
record_transaction(transaction)NoRecord a fill.
clear_transactions()NoClear all recorded transactions.
prune_orders(context)NoClean up stale orders. Default is a no-op.